﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Drawing;
using Exocortex.DSP;

namespace DinletBilsin
{
    public class DinletBilsinCore
    {
        private static int BINARY_HEADER = -1;

        private List<string> songList = new List<string>();
        private Dictionary<long, List<FingerPrint>> hashDictionary = new Dictionary<long, List<FingerPrint>>();

        HashGenerator hashGenerator;

        public DinletBilsinCore(HashGenerator hashGen)
        {
            hashGenerator = hashGen;
        }

        #region Veritabanı Oluşturmak İçin

        public void AddSong(string path)
        {
            try
            {
                int id = songList.Count;
                byte[] audio = File.ReadAllBytes(path);
                long[] hashList = hashGenerator.GetHashList(CalculateFFT(audio, 0, audio.Length, hashGenerator.ChunkSize));

                AddHash(id, hashList);
                string name = Path.GetFileNameWithoutExtension(path);
                songList.Add(name);
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

        private void AddHash(int songID, long[] hashList)
        {
            for (int i = 0, size = hashList.Length; i < size; i++)
            {
                long hash = hashList[i];
                List<FingerPrint> pointList = null;
                if (hashDictionary.ContainsKey(hash))
                    pointList = hashDictionary[hash];
                else
                {
                    pointList = new List<FingerPrint>();
                    hashDictionary.Add(hash, pointList);
                }
                // hashler sıralı yerleşir
                pointList.Add(new FingerPrint((short)i, (short)songID));
            }
        }
        /// <summary>
        /// Veritabanı oluşturur.
        /// </summary>
        /// <param name="fileName"></param>
      
        public void Save(string fileName)
        {         
            try
            {             
                using (StreamWriter sw = new StreamWriter(fileName, false))
                {
                    sw.WriteLine(songList.Count);
                    foreach (string name in songList)
                    {
                        sw.WriteLine(name);
                    }

                    sw.WriteLine(hashDictionary.Count);
                    Dictionary<long, List<FingerPrint>>.Enumerator en = hashDictionary.GetEnumerator();
                    while (en.MoveNext())
                    {
                        KeyValuePair<long, List<FingerPrint>> current = en.Current;
                        sw.Write(current.Key);
                        foreach (FingerPrint dataPoint in current.Value)
                        {
                            sw.Write(string.Format("\t{0},{1}", dataPoint.Time, dataPoint.SongID));
                        }
                        sw.WriteLine();
                    }
                    sw.Flush();
                }
             
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }
        #endregion


        /// <summary>
        /// Veritabanını yükler.       
        /// </summary>
        /// <param name="fileName"></param>
        public void Load(string fileName)
        {
            try
            {
                using (FileStream fr = new FileStream(fileName, FileMode.Open))
                {
                    BinaryReader br = new BinaryReader(fr);
                    songList.Clear();

                    int header = br.ReadInt32();
                    if (header != BINARY_HEADER)
                    {
                        throw new InvalidDataException("Invalid index file!");
                    }

                    int nameCount = br.ReadInt32();
                    for (int i = 0; i < nameCount; i++)
                    {
                        string name = br.ReadString();
                        songList.Add(name);
                    }

                    hashDictionary.Clear();
                    int hashCount = br.ReadInt32();
                    for (int i = 0; i < hashCount; i++)
                    {
                        long key = br.ReadInt64();
                        int count = br.ReadInt32();

                        List<FingerPrint> pointList = new List<FingerPrint>(count);
                        for (int j = 0; j < count; j++)
                        {
                            short time = br.ReadInt16();
                            short songID = br.ReadInt16();
                            FingerPrint dataPoint = new FingerPrint(time, songID);
                            pointList.Add(dataPoint);
                        }

                        hashDictionary.Add(key, pointList);
                    }
                }
            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }
        /// <summary>
        /// Aramanın yapıldığı başlangıç noktası.
        /// </summary>
        /// <param name="recordPath"></param>
        /// <param name="core"></param>
        /// <param name="isRawData">Eğer raw yollandıysa true set edilmeli, .wav geldiyse false set edilmeli.</param>
        /// <returns></returns>
        public static string Search(String recordPath, DinletBilsinCore core, bool isRawData)
        {
            int id = -1;

            byte[] audio = File.ReadAllBytes(recordPath);
            if (!isRawData)
            {
                byte[] audioNew = new byte[audio.Length - 40];
                Buffer.BlockCopy(audio, 40, audioNew, 0, audioNew.Length);
                id = core.GetSongID(audioNew);
            }
            else
                id = core.GetSongID(audio);
            if (id < 0)
                return "No match!";
            else
                return core.GetNameByID(id);
        }
        /// <summary>
        /// Byte dizisi kullanır. WAV Upload kullanıldığında tercih edilmesi lazım. En iyi şarkı ID sini döndürür.
        /// </summary>
        /// <param name="audio"></param>
        /// <returns></returns>
        public int GetSongID(byte[] audio)
        {
            int start = 0;
            int length = audio.Length;
            int chunkSize = hashGenerator.ChunkSize;
            int stepSize = chunkSize / Util.ShiftCount;

            HitCounter counter = new HitCounter();

            int maxScore = -1;
            int bestId = -1;
            for (int i = 0; i < chunkSize; i += stepSize)
            {
                int startIndex = start + i;
                int newLength = length - i;
                Dictionary<int, List<int>> results = AnalyseSong(audio, startIndex, newLength);
                int score = 0;
                List<KeyValuePair<int, int>> bestHit = GetMaxHit(results);
                int id = -1;
                if (bestHit.Count > 0)
                {
                    id = bestHit[0].Value;
                    score = bestHit[0].Key;
                }
                counter.Update(bestHit);
                if (score > maxScore)
                {
                    maxScore = score;
                    bestId = id;
                }
            }
            int scoreCounter = 0;
            int maxID = counter.GetMaxID(ref scoreCounter);
            if (maxScore < 5 && maxID != bestId)
            {
                bestId = -1;
            }

            return bestId;
        }
        /// <summary>
        ///  Hash dizisi kullanır. Hash dizisi sunucuya geldiğinde tercih edilmesi lazım. En iyi şarkı ID sini döndürür.
        /// </summary>
        /// <param name="hashesList"></param>
        /// <param name="hopCount">Hash oluşturulurken yapılan atlama sayısı</param>
        /// <returns></returns>
        public int GetSongID(long[] hashesList, int hopCount)
        {

            HitCounter counter = new HitCounter();

            int maxScore = -1;
            int bestId = -1;
            int stepSize = hashGenerator.ChunkSize / Util.ShiftCount;

            for (int i = 0; i < stepSize; i++)
            {
                long[] hashes = new long[hopCount];

                for (int x = 0; x < hashes.Length; x++)
                {
                    hashes[x] = hashesList[i * hopCount + x];
                }

                Dictionary<int, List<int>> results = AnalyseSong(hashes);
                int score = 0;
                List<KeyValuePair<int, int>> bestHit = GetMaxHit(results);
                int id = -1;
                if (bestHit.Count > 0)
                {
                    id = bestHit[0].Value;
                    score = bestHit[0].Key;
                }
                counter.Update(bestHit);
                if (score > maxScore)
                {
                    maxScore = score;
                    bestId = id;
                }
            }
            int scoreMany = 0;
            int idMany = counter.GetMaxID(ref scoreMany);
            if (maxScore < 5 && idMany != bestId)
            {
                bestId = -1;
            }

            return bestId;
        }


        public string GetNameByID(int id)
        {
            return songList[id];
        }
        /// <summary>
        /// Hash üretimini ve karşılaştırmalarını yapar.      
        /// Kısaca hashleri analiz eder.
        /// WAV kullanıldığında kullanılır.
        /// </summary>
        /// <param name="audio"></param>
        /// <param name="start"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public Dictionary<int, List<int>> AnalyseSong(byte[] audio, int start, int length)
        {
            Dictionary<int, List<int>> results = new Dictionary<int, List<int>>();
            long[] hashes = hashGenerator.GetHashList(CalculateFFT(audio, start, length, hashGenerator.ChunkSize));
            for (int i = 0; i < hashes.Length; i++)
            {
                if (hashDictionary.ContainsKey(hashes[i]))
                {
                    List<FingerPrint> fingerPrintList = hashDictionary[hashes[i]];
                    // Şarkının hash - time ilişkisi burda belirlenir. Veritabanı oluştururken zaten sıralı geliyor.
                    foreach (FingerPrint songPoint in fingerPrintList)
                    {
                        List<int> hits = null;
                        if (results.ContainsKey(songPoint.SongID))
                            hits = results[songPoint.SongID];
                        else
                        {
                            hits = new List<int>();
                            results.Add(songPoint.SongID, hits);
                        }
                        // çok önemli !!
                        hits.Add(songPoint.Time - i);
                    }
                }
            }
            return results;
        }
        /// <summary>
        /// Hash dizisi gelirse ordan şarkıyı arar. Dizi kullanılacağı zaman kullanılması lazım. 
        /// Telde hash çıkarımı sonucu buraya verilir servis tarafından.
        /// Hash çıkarımı işlemi kaldırılmış özel bir versiyon.
        /// </summary>
        /// <param name="hashes"></param>
        /// <returns></returns>
        public Dictionary<int, List<int>> AnalyseSong(long[] hashes)
        {
            Dictionary<int, List<int>> results = new Dictionary<int, List<int>>();

            for (int i = 0; i < hashes.Length; i++)
            {
                if (hashDictionary.ContainsKey(hashes[i]))
                {
                    List<FingerPrint> fingerPrintList = hashDictionary[hashes[i]];
                    // Şarkının hash - time ilişkisi burda belirlenir. Veritabanı oluştururken zaten sıralı geliyor.
                    foreach (FingerPrint songPoint in fingerPrintList)
                    {
                        List<int> hits = null;
                        if (results.ContainsKey(songPoint.SongID))
                            hits = results[songPoint.SongID];
                        else
                        {
                            hits = new List<int>();
                            results.Add(songPoint.SongID, hits);
                        }
                        // çok önemli !!
                        hits.Add(songPoint.Time - i);
                    }
                }
            }
            return results;
        }
        /// <summary>
        /// Puanlama için önemli kısım.
        /// Analiz sonucu çıkan noktaları sıralar ve en iyi hiti bulur.
        /// Filtreleme yapar. 10 dan fazla eşleşme yoksa bakmaz.
        /// </summary>
        /// <param name="results"></param>
        /// <returns></returns>
        public List<KeyValuePair<int, int>> GetMaxHit(Dictionary<int, List<int>> results)
        {
            List<KeyValuePair<int, int>> maxRowID = new List<KeyValuePair<int, int>>();
            foreach (KeyValuePair<int, List<int>> item in results)
            {
                if (item.Value.Count >= 10)
                {
                    item.Value.Sort();
                    List<KeyValuePair<int, int>> countSpan = GetBestTwoSpan(item.Value);
                    // Şarkı benzemeye devam ediyor mu kontrolü? 
                    // Eğer ard arda gelen noktaların yerlerinin farkı 10 dan küçükse 
                    if (countSpan.Count >= 2 && Math.Abs(countSpan[0].Value - countSpan[1].Value) < 10)
                        maxRowID.Add(new KeyValuePair<int, int>(countSpan[0].Key + countSpan[1].Key, item.Key));
                    // Tek sonuç dönerse
                    else if (countSpan.Count > 0)
                        maxRowID.Add(new KeyValuePair<int, int>(countSpan[0].Key, item.Key));
                }
            }


            // Sıralama yaparken keylere göre sıralar.
            maxRowID.Sort(new Comparison<KeyValuePair<int, int>>((KeyValuePair<int, int> firstPair, KeyValuePair<int, int> secondPair)
                       =>
                       {
                           return secondPair.Key.CompareTo(firstPair.Key);
                       }));



            return maxRowID;
        }

        /// <summary>
        /// FFT Fonksiyonu
        /// </summary>
        /// <param name="data"></param>
        /// <param name="index"></param>
        /// <param name="length"></param>
        /// <param name="chunkSize"></param>
        /// <returns></returns>
        public static Complex[][] CalculateFFT(byte[] data, int index, int length, int chunkSize)
        {
            int totalSize = length;
            int rowSize = totalSize / chunkSize;
                       
            Complex[][] results = new Complex[rowSize][];
                       
            for (int i = 0; i < rowSize; i++)
            {
                Complex[] complex = new Complex[chunkSize];

                for (int j = 0; j < chunkSize; j++)
                {                  
                    if (complex[j] == null)
                        complex[j] = new Complex(data[index++], 0);
                    else
                    {
                        complex[j].Re = data[index++];
                        complex[j].Im = 0;
                    }
                }
                              
                Fourier.FFT(complex, complex.Length, FourierDirection.Forward);
                results[i] = complex;
            }

            return results;
        }
        /// <summary>
        /// Son art arda geçen iki zaman bulunur.
        /// Art arda gelen zamanlar puanı artırır.
        /// Bu metod düzenlenebilir. karmaşıklığı azaltılabilir.
        /// </summary>
        /// <param name="timePoints">Analiz sonucu çıkan zamanlar, buraya sıralı verilmesi gerek</param>
        /// <returns></returns>
        private List<KeyValuePair<int, int>> GetBestTwoSpan(List<int> timePoints)
        {
            List<KeyValuePair<int, int>> countSpan = new List<KeyValuePair<int, int>>();
            int firstTime, firstValue;
            int secondTime, secondValue;
            firstTime = secondTime = 0;
            firstValue = 100;
            secondValue = -100;

            int count = 0;
            int value = int.MaxValue;
           
            foreach (int time in timePoints)
            {
                if (value == int.MaxValue)
                {
                    value = time;
                    count = 1;
                }
                else
                {
                    if (time == value)
                    {
                        count++;
                    }
                    else
                    {
                        if (count > firstTime)
                        {
                            Swap(ref firstTime, ref secondTime);
                            Swap(ref firstValue, ref secondValue);

                            firstTime = count;
                            firstValue = value;
                        }
                        else if (count > secondTime)
                        {
                            secondTime = count;
                            secondValue = value;
                        }
                        count = 1;
                        value = time;
                    }
                }
            }
            if (count > firstTime)
            {
                Swap(ref firstTime, ref secondTime);
                Swap(ref firstValue, ref secondValue);

                firstTime = count;
                firstValue = value;
            }
            else if (count > secondTime)
            {
                secondTime = count;
                secondValue = value;
            }

            countSpan.Add(new KeyValuePair<int, int>(firstTime, firstValue));
            countSpan.Add(new KeyValuePair<int, int>(secondTime, secondValue));

            return countSpan;
        }
       
        private void Swap(ref int x, ref int y)
        {
            int tmp = x;
            x = y;
            y = tmp;
        }
        /// <summary>
        /// Servis tarafından çağrılır.
        /// </summary>
        /// <param name="recordPath"></param>
        /// <param name="isCached">Global.asax cache yaptıysa true olur</param>
        /// <param name="hashes">Hash dizisi gelirse buraya verilir</param>
        /// <param name="isRawData">Raw dataysa true yapılır</param>
        /// <returns></returns>
        public static string ReturnResult(String recordPath, Boolean isCached, Int64[] hashes, bool isRawData = false)
        {
            String infoTrack = "";
            DinletBilsinCore dataBase = new DinletBilsinCore(new HashGenerator());
            if (isCached)
            {
                dataBase.hashDictionary = Util.cacheHashMap;
                dataBase.songList = Util.cacheSongNames;
            }
            else
                dataBase.Load(Util.DataBasePath);
            if (hashes == null)
            {
                infoTrack = DinletBilsinCore.Search(recordPath, dataBase, isRawData);
                File.Delete(recordPath);
                return infoTrack;
            }
            else
            {

                int id = dataBase.GetSongID(hashes, 129);
                if (id < 0)
                    return "No match!";
                else
                    return dataBase.GetNameByID(id);

            }

        }
    }
}
